package com.dbflow5.database;

import com.dbflow5.DatabaseFileUtils;
import com.dbflow5.config.DBFlowDatabase;
import com.dbflow5.config.FlowLog;
import ohos.app.Context;
import ohos.global.resource.RawFileEntry;

import java.io.*;
import java.util.function.Function;

/**
 * Description: An abstraction from some parts of the Ohos SQLiteOpenHelper where this can be
 * used in other helper class definitions.
 */
public class LocalDatabaseHelperDelegate extends LocalDatabaseHelper implements OpenHelperDelegate {
    public static final String TEMP_DB_NAME = "temp-";

    private final Context context;
    private DatabaseCallback databaseCallback;
    private final OpenHelper backupHelper;

    public LocalDatabaseHelperDelegate(Context context, DatabaseCallback databaseCallback, DBFlowDatabase databaseDefinition, OpenHelper backupHelper){
        super(new OhosMigrationFileHelper(context), databaseDefinition);
        this.context = context;
        this.databaseCallback = databaseCallback;
        this.backupHelper = backupHelper;
    }

    /**
     * tempDbFileName
     *
     * @return the temporary database file name for when we have backups enabled [DBFlowDatabase.backupEnabled]
     */
    private String tempDbFileName(){
        return getTempDbFileName(databaseDefinition);
    }

    /**
     * Pulled partially from code, it runs a "PRAGMA quick_check(1)" to see if the database is ok.
     * This method will [.restoreBackUp] if they are enabled on the database if this check fails. So
     * use with caution and ensure that you backup the database often!
     *
     * @return true if the database is ok, false if the consistency has been compromised.
     */
    @Override
    public boolean isDatabaseIntegrityOk(){
        return isDatabaseIntegrityOk(database());
    }

    @Override
    public DatabaseWrapper database() {
        return databaseDefinition;
    }

    @Override
    public void performRestoreFromBackup() {
        movePrepackagedDB(databaseDefinition.getDatabaseFileName(),
            databaseDefinition.getDatabaseFileName());

        if (databaseDefinition.backupEnabled()) {
            if (backupHelper == null) {
                throw new IllegalStateException("the passed backup helper was null, even though backup" +
                    " is enabled. Ensure that its passed in.");
            }
            restoreDatabase(tempDbFileName(), databaseDefinition.getDatabaseFileName());
            backupHelper.database();
        }
    }

    @Override
    public LocalDatabaseHelperDelegate delegate() {
        return this;
    }

    /**
     * setDatabaseHelperListener
     *
     * @param databaseCallback Listens for operations the DB and allow you to provide extra functionality
     */
    public void setDatabaseHelperListener(DatabaseCallback databaseCallback) {
        this.databaseCallback = databaseCallback;
    }

    public void onConfigure(DatabaseWrapper db) {
        databaseCallback.onConfigure(db);
        super.onConfigure(db);
    }

    public void onCreate(DatabaseWrapper db) {
        databaseCallback.onCreate(db);
        super.onCreate(db);
    }

    public void onUpgrade(DatabaseWrapper db, int oldVersion, int newVersion) {
        databaseCallback.onUpgrade(db, oldVersion, newVersion);
        super.onUpgrade(db, oldVersion, newVersion);
    }

    public void onOpen(DatabaseWrapper db) {
        databaseCallback.onOpen(db);
        super.onOpen(db);
    }

    public void onDowngrade(DatabaseWrapper db, int oldVersion, int newVersion) {
        databaseCallback.onDowngrade(db, oldVersion, newVersion);
        super.onDowngrade(db, oldVersion, newVersion);
    }

    /**
     * Copies over the prepackaged DB into the main DB then deletes the existing DB to save storage space. If
     * we have a backup that exists
     *
     * @param databaseName    The name of the database to copy over
     * @param prepackagedName The name of the prepackaged db file
     */
    public void movePrepackagedDB(String databaseName, String prepackagedName) {
        File dbPath = DatabaseFileUtils.getDatabasePath(context, databaseName);

        // If the database already exists, and is ok return
        if (dbPath.exists()
            && (!databaseDefinition.areConsistencyChecksEnabled()
                || (databaseDefinition.areConsistencyChecksEnabled() && isDatabaseIntegrityOk(database())))) {
            return;
        }

        // Make sure we have a path to the file
        boolean mkdirs = dbPath.getParentFile().mkdirs();

        // Try to copy database file
        try {
            // check existing and use that as backup
            File existingDb = DatabaseFileUtils.getDatabasePath(context, tempDbFileName());
            // if it exists and the integrity is ok we use backup as the main DB is no longer valid
            InputStream inputStream;
            if (existingDb.exists()
                && (!databaseDefinition.backupEnabled() ||
                    (databaseDefinition.backupEnabled()
                        && backupHelper != null
                        && isDatabaseIntegrityOk(backupHelper.database())))) {
                inputStream = new FileInputStream(existingDb);
            } else {
                RawFileEntry rawFileEntry = context.getResourceManager().getRawFileEntry("entry/resources/rawfile/" + prepackagedName);
                inputStream = rawFileEntry.openRawFile();
            }
            writeDB(dbPath, inputStream);
        } catch (IOException e) {
            FlowLog.log(FlowLog.Level.W, "Failed to open file:"+e.getMessage(), e);
        }

    }


    /**
     * Will use the already existing app database if [DBFlowDatabase.backupEnabled] is true. If the existing
     * is not there we will try to use the prepackaged database for that purpose.
     *
     * @param databaseName    The name of the database to restore
     * @param prepackagedName The name of the prepackaged db file
     */
    public void restoreDatabase(String databaseName, String prepackagedName) {
        File dbPath = DatabaseFileUtils.getDatabasePath(context, databaseName);

        // If the database already exists, return
        if (dbPath.exists()) {
            return;
        }

        // Make sure we have a path to the file
        boolean mkdirs = dbPath.getParentFile().mkdirs();

        // Try to copy database file
        try {
            // check existing and use that as backup
            File existingDb = DatabaseFileUtils.getDatabasePath(context, databaseDefinition.getDatabaseFileName());
            // if it exists and the integrity is ok
            InputStream inputStream = null;
            if (existingDb.exists()
                && (databaseDefinition.backupEnabled() && backupHelper != null
                    && isDatabaseIntegrityOk(backupHelper.database()))) {
                inputStream = new FileInputStream(existingDb);
            } else {
                RawFileEntry rawFileEntry = context.getResourceManager().getRawFileEntry("entry/resources/rawfile/" + prepackagedName);
                inputStream = rawFileEntry.openRawFile();
            }
            writeDB(dbPath, inputStream);
        } catch (IOException e) {
            FlowLog.logError(e);
        }

    }

    /**
     * Pulled partially from code, it runs a "PRAGMA quick_check(1)" to see if the database is ok.
     * This method will [.restoreBackUp] if they are enabled on the database if this check fails. So
     * use with caution and ensure that you backup the database often!
     *
     * @param databaseWrapper databaseWrapper
     * @return true if the database is ok, false if the consistency has been compromised.
     */
    public boolean isDatabaseIntegrityOk(DatabaseWrapper databaseWrapper) {
        boolean integrityOk = true;
        DatabaseStatement statement = databaseWrapper.compileStatement("PRAGMA quick_check(1)");
        String result = statement.simpleQueryForString();
        if (result == null || !result.equalsIgnoreCase("ok")) {
            // integrity_checker failed on main or attached databases
            FlowLog.log(FlowLog.Level.E, "PRAGMA integrity_check on "+ databaseDefinition.getDatabaseName() +" returned: "+ result);
            integrityOk = false;
            if (databaseDefinition.backupEnabled()) {
                integrityOk = restoreBackUp();
            }
        }
        return integrityOk;
    }

    /**
     * If integrity check fails, this method will use the backup db to fix itself. In order to prevent
     * loss of data, please backup often!
     *
     * @return success
     */
    public boolean restoreBackUp() {
        boolean success = true;

        File db = DatabaseFileUtils.getDatabasePath(context,TEMP_DB_NAME + databaseDefinition.getDatabaseName());
        File corrupt = DatabaseFileUtils.getDatabasePath(context, databaseDefinition.getDatabaseName());
        if (corrupt.delete()) {
            try {
                writeDB(corrupt, new FileInputStream(db));
            } catch (IOException e) {
                FlowLog.logError(e);
                success = false;
            }

        } else {
            FlowLog.log(FlowLog.Level.E, "Failed to delete DB");
        }
        return success;
    }

    /**
     * Writes the [InputStream] of the existing db to the file specified.
     *
     * @param dbPath     The file to write to.
     * @param existingDB The existing database file's input stream¬
     * @throws IOException IOException
     */
    private void writeDB(File dbPath, InputStream existingDB) throws IOException {
        OutputStream output = new FileOutputStream(dbPath);

        byte[] buffer = new byte[1024];
        int length = existingDB.read(buffer);
        while (length > 0) {
            output.write(buffer, 0, length);
            length = existingDB.read(buffer);
        }

        output.flush();
        output.close();
        existingDB.close();
    }

    /**
     * Saves the database as a backup on the [DefaultTransactionQueue].
     * This will create a THIRD database to use as a backup to the backup in case somehow the overwrite fails.
     */
    @Override
    public void backupDB() {
        if (!databaseDefinition.backupEnabled() || !databaseDefinition.areConsistencyChecksEnabled()) {
            throw new IllegalStateException("Backups are not enabled for : " +
                  databaseDefinition.getDatabaseName() + ". Please consider adding both backupEnabled " +
                  "and consistency checks enabled to the Database annotation");
        }

        databaseDefinition.beginTransactionAsync(new Function<DatabaseWrapper, Object>() {
            @Override
            public Object apply(DatabaseWrapper databaseWrapper) {
                File backup = DatabaseFileUtils.getDatabasePath(context, tempDbFileName());
                File temp = DatabaseFileUtils.getDatabasePath(context,TEMP_DB_NAME + "-2-" + databaseDefinition.getDatabaseFileName());

                // if exists we want to delete it before rename
                if (temp.exists()) {
                    boolean isTempDelete = temp.delete();
                }

                boolean renameTo = backup.renameTo(temp);
                if (backup.exists()) {
                    boolean isBackupDelete = backup.delete();
                }

                File existing = DatabaseFileUtils.getDatabasePath(context, databaseDefinition.getDatabaseFileName());
                try {
                    boolean isBackupDirs = backup.getParentFile().mkdirs();
                    writeDB(backup, new FileInputStream(existing));
                    boolean isDelete = temp.delete();
                } catch (Exception e) {
                    FlowLog.logError(e);
                }
                return null;
            }
        }).execute(null, null, null, null);
    }

    public static String getTempDbFileName(DBFlowDatabase databaseDefinition) {
        return TEMP_DB_NAME + databaseDefinition.getDatabaseName() + ".db";
    }
}
